# Low level unix utility functions
#
# Copyright (C) 2016-2020  Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import sys, os, pty, fcntl, termios, signal, logging, json, time
import subprocess, traceback, shlex

# 自定义的 util.py 文件, 一般用于存放一些通用的、辅助性的工具函数和类，这些功能可以在项目的多个地方被复用
######################################################################
# Low-level Unix commands
######################################################################

# Return the SIGINT interrupt handler back to the OS default
def fix_sigint():
    signal.signal(signal.SIGINT, signal.SIG_DFL)
fix_sigint()

# Set a file-descriptor as non-blocking
def set_nonblock(fd):
    fcntl.fcntl(fd, fcntl.F_SETFL
                , fcntl.fcntl(fd, fcntl.F_GETFL) | os.O_NONBLOCK)

# Clear HUPCL flag
def clear_hupcl(fd):
    attrs = termios.tcgetattr(fd)
    attrs[2] = attrs[2] & ~termios.HUPCL
    try:
        termios.tcsetattr(fd, termios.TCSADRAIN, attrs)
    except termios.error:
        pass

# Support for creating a pseudo-tty for emulating a serial port
def create_pty(ptyname):
    mfd, sfd = pty.openpty()
    try:
        os.unlink(ptyname)
    except os.error:
        pass
    filename = os.ttyname(sfd)
    os.chmod(filename, 0o660)
    os.symlink(filename, ptyname)
    set_nonblock(mfd)
    old = termios.tcgetattr(mfd)
    old[3] = old[3] & ~termios.ECHO
    termios.tcsetattr(mfd, termios.TCSADRAIN, old)
    return mfd


######################################################################
# Helper code for extracting mcu build info
######################################################################

def dump_file_stats(build_dir, filename):
    fname = os.path.join(build_dir, filename)
    try:
        mtime = os.path.getmtime(fname)
        fsize = os.path.getsize(fname)
        timestr = time.asctime(time.localtime(mtime))
        logging.info("Build file %s(%d): %s", fname, fsize, timestr)
    except:
        logging.info("No build file %s", fname)

# Try to log information on the last mcu build
def dump_mcu_build():
    build_dir = os.path.join(os.path.dirname(__file__), '..')
    # Try to log last mcu config
    dump_file_stats(build_dir, '.config')
    try:
        f = open(os.path.join(build_dir, '.config'), 'r')
        data = f.read(32*1024)
        f.close()
        logging.info("========= Last MCU build config =========\n%s"
                     "=======================", data)
    except:
        pass
    # Try to log last mcu build version
    dump_file_stats(build_dir, 'out/klipper.dict')
    try:
        f = open(os.path.join(build_dir, 'out/klipper.dict'), 'r')
        data = f.read(32*1024)
        f.close()
        data = json.loads(data)
        logging.info("Last MCU build version: %s", data.get('version', ''))
        logging.info("Last MCU build tools: %s", data.get('build_versions', ''))
        cparts = ["%s=%s" % (k, v) for k, v in data.get('config', {}).items()]
        logging.info("Last MCU build config: %s", " ".join(cparts))
    except:
        pass
    dump_file_stats(build_dir, 'out/klipper.elf')


######################################################################
# Python2 wrapper hacks
######################################################################

def setup_python2_wrappers():
    if sys.version_info.major >= 3:
        return
    # Add module hacks so that common Python3 module imports work in Python2
    import ConfigParser, Queue, io, StringIO, time
    sys.modules["configparser"] = ConfigParser
    sys.modules["queue"] = Queue
    io.StringIO = StringIO.StringIO
    time.process_time = time.clock
setup_python2_wrappers()


######################################################################
# General system and software information
######################################################################

def get_cpu_info():
    try:
        f = open('/proc/cpuinfo', 'r')
        data = f.read()
        f.close()
    except (IOError, OSError) as e:
        logging.debug("Exception on read /proc/cpuinfo: %s",
                      traceback.format_exc())
        return "?"
    lines = [l.split(':', 1) for l in data.split('\n')]
    lines = [(l[0].strip(), l[1].strip()) for l in lines if len(l) == 2]
    core_count = [k for k, v in lines].count("processor")
    model_name = dict(lines).get("model name", "?")
    return "%d core %s" % (core_count, model_name)

def get_version_from_file(klippy_src):
    try:
        with open(os.path.join(klippy_src, '.version')) as h:
            return h.read().rstrip()
    except IOError:
        pass
    return "?"

def _get_repo_info(gitdir):
    repo_info = {"branch": "?", "remote": "?", "url": "?"}
    prog_branch = ('git', '-C', gitdir, 'branch', '--no-color')
    try:
        process = subprocess.Popen(prog_branch, stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
        branch_list, err = process.communicate()
        retcode = process.wait()
        if retcode != 0:
            logging.debug("Error running git branch: %s", err)
            return repo_info
        lines = str(branch_list.strip().decode()).split("\n")
        for line in lines:
            if line[0] == "*":
                repo_info["branch"] = line[1:].strip()
                break
        else:
            logging.debug("Unable to find current branch:\n%s", branch_list)
            return repo_info
        if repo_info["branch"].startswith("(HEAD detached"):
            parts = repo_info["branch"].strip("()").split()[-1].split("/", 1)
            if len(parts) != 2:
                return repo_info
            repo_info["remote"] = parts[0]
        else:
            key = "branch.%s.remote" % (repo_info["branch"],)
            prog_config = ('git', '-C', gitdir, 'config', '--get', key)
            process = subprocess.Popen(prog_config, stdout=subprocess.PIPE,
                                       stderr=subprocess.PIPE)
            remote_info, err = process.communicate()
            retcode = process.wait()
            if retcode != 0:
                logging.debug("Error running git config: %s", err)
                return repo_info
            repo_info["remote"] = str(remote_info.strip().decode())
        prog_remote_url = (
            'git', '-C', gitdir, 'remote', 'get-url', repo_info["remote"])
        process = subprocess.Popen(prog_remote_url, stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
        remote_url, err = process.communicate()
        retcode = process.wait()
        if retcode != 0:
            logging.debug("Error running git remote get-url: %s", err)
            return repo_info
        repo_info["url"] = str(remote_url.strip().decode())
    except:
        logging.debug("Error fetching repo info: %s", traceback.format_exc())
    return repo_info

#####################################################
# 语法知识 try except else finally

# 1. 基本概念
# 在 Python 中，try-except-else-finally 语句用于处理程序运行时可能出现的异常。
# 异常是程序执行过程中发生的错误，会导致程序终止。使用这些语句可以捕获异常并采取相应措施，避免程序崩溃。

# 2. 基本语法结构

# 2.1 单异常捕获
# try 块包含可能会抛出异常的代码，Python 会尝试执行其中的代码。
# 当 try 块中的代码抛出指定类型的异常时，程序会跳转到对应的 except 块中执行处理代码。
    # try:
    #     # 可能会抛出异常的代码
    #     num = int("abc")  # 这里会抛出 ValueError 异常
    # except ValueError:
    #     print("输入不是有效的整数！")

# 2.2 多异常捕获
# 可以使用元组指定多个异常类型，当其中任意一种异常发生时，执行相应的 except 块。
# 也可以分别处理不同类型的异常。
    # try:
    #     result = 10 / 0  # 这里会抛出 ZeroDivisionError 异常
    #     num = int("abc")  # 这里会抛出 ValueError 异常
    # except (ZeroDivisionError, ValueError) as e:
    #     print(f"发生异常: {e}")

# 或者分别处理不同异常
    # try:
    #     result = 10 / 0
    #     num = int("abc")
    # except ZeroDivisionError:
    #     print("除数不能为零！")
    # except ValueError:
    #     print("输入不是有效的整数！")

# 2.3 捕获所有异常
# 不推荐直接使用 except 捕获所有异常，因为会捕获系统退出异常等，隐藏真正问题。
# 推荐使用 except Exception 捕获所有常规异常。
    # try:
    #     num = int("abc")
    # except Exception as e:
    #     print(f"发生异常: {e}")

# 3. else 子句
# else 子句在 try 块中的代码没有抛出任何异常时执行。
    # try:
    #     num = int("123")
    # except ValueError:
    #     print("输入不是有效的整数！")
    # else:
    #     print(f"输入的整数是: {num}")

# 4. finally 子句
# finally 子句无论 try 块中是否抛出异常，都会执行。
    # try:
    #     num = int("abc")
    # except ValueError:
    #     print("输入不是有效的整数！")
    # finally:
    #     print("无论是否发生异常，这里的代码都会执行。")

# 5. 完整示例
# 结合 try、except、else 和 finally 子句的完整示例
    # try:
    #     print("开始执行 try 语句块")
    #     num = int(input("请输入一个整数: "))
    #     result = 10 / num
    # except ValueError:
    #     print("输入不是有效的整数！")
    # except ZeroDivisionError:
    #     print("除数不能为零！")
    # else:
    #     print(f"计算结果: {result}")
    # finally:
    #     print("程序结束，finally 块执行完毕。")
#####################################################
#####################################################
# 语法知识补充 - 列表推导式
#
# 列表推导式（List Comprehension）是 Python 中一种简洁、高效的创建列表的方式，
# 它允许你在一行代码内通过对一个可迭代对象（如列表、元组、字符串等）进行遍历和操作来生成一个新的列表。
#
# 1. 基本语法
# 列表推导式的基本形式为：
# [expression for item in iterable]
# - expression：这是一个表达式，用于对从 iterable 中取出的每个 item 进行处理，处理结果会作为新列表的元素。
# - item：是一个临时变量，用于在迭代过程中表示从 iterable 中取出的单个元素。
# - iterable：是一个可迭代对象，例如列表、元组、字符串、集合等。
#
# 示例代码：
# 生成一个包含 0 到 4 的平方的列表
# squares = [x**2 for x in range(5)]
# print(squares)  # 输出: [0, 1, 4, 9, 16]
#
# 2. 带有条件判断的列表推导式
# 可以在列表推导式中添加条件判断，只对满足条件的元素进行处理并添加到新列表中。其语法形式为：
# [expression for item in iterable if condition]
# - condition：是一个布尔表达式，用于筛选出满足条件的 item。只有当 condition 为 True 时，expression 才会被执行并将结果添加到新列表中。
#
# 示例代码：
# 生成一个包含 0 到 9 中所有偶数的平方的列表
# even_squares = [x**2 for x in range(10) if x % 2 == 0]
# print(even_squares)  # 输出: [0, 4, 16, 36, 64]
#
# 3. 嵌套循环的列表推导式
# 列表推导式还可以包含嵌套循环，用于处理多维可迭代对象或组合多个可迭代对象的元素。其语法形式为：
# [expression for item1 in iterable1 for item2 in iterable2]
# 这种形式会先遍历 iterable1 中的每个元素 item1，对于每个 item1，再遍历 iterable2 中的每个元素 item2，
# 并对 item1 和 item2 执行 expression 操作，将结果添加到新列表中。
#
# 示例代码：
# 生成一个由两个列表元素组合而成的新列表
# list1 = [1, 2, 3]
# list2 = ['a', 'b']
# combined = [(x, y) for x in list1 for y in list2]
# print(combined)  # 输出: [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b'), (3, 'a'), (3, 'b')]
#
# 4. 带有嵌套条件判断的列表推导式
# 可以在嵌套循环的列表推导式中添加条件判断，进一步筛选元素。语法形式可能如下：
# [expression for item1 in iterable1 if condition1 for item2 in iterable2 if condition2]
#
# 示例代码：
# 生成一个由两个列表元素组合而成的新列表，只包含满足特定条件的组合
# list1 = [1, 2, 3]
# list2 = [4, 5, 6]
# filtered_combined = [(x, y) for x in list1 if x % 2 == 1 for y in list2 if y > 5]
# print(filtered_combined)  # 输出: [(1, 6), (3, 6)]
#
# 总结
# 列表推导式是 Python 中一种强大且灵活的工具，它可以让你用简洁的代码实现复杂的列表生成逻辑。
# 通过合理运用条件判断和嵌套循环，你可以根据不同的需求生成各种形式的列表。
# 但需要注意的是，过于复杂的列表推导式可能会降低代码的可读性，在这种情况下，使用普通的 for 循环可能会更合适。
#####################################################

def get_git_version(from_file=True):
    git_info = {
        "version": "?",
        "file_status": [],
        "branch": "?",# 分支
        "remote": "?",# 远程
        "url": "?"
    }
    # 获取当前文件所在位置的路径
    klippy_src = os.path.dirname(__file__)

    # Obtain version info from "git" program
    gitdir = os.path.join(klippy_src, '..')
    prog_desc = ('git', '-C', gitdir, 'describe', '--always',
                 '--tags', '--long', '--dirty') # 这个命令围绕当前提交展开，通过查找标签、统计提交数量、检查工作目录状态以及使用提交哈希值等方式，全面而准确地描述了当前提交的相关信息。
    prog_status = ('git', '-C', gitdir, 'status', '--porcelain', '--ignored') #这个行的作用是列出当前未保存的变更
    try:
        # subprocess.Popen 是 Python 标准库 subprocess 模块中的一个函数，它主要用于创建新的进程并与之进行交互，这在需要调用外部命令时非常有用。
        # stdout 参数用于指定子进程的标准输出（也就是命令执行后的正常输出信息）的处理方式。
        # stderr 参数用于指定子进程的标准错误输出（也就是命令执行过程中出现错误时的输出信息）的处理方式。
        # subprocess.PIPE 是一个特殊的值，表示将子进程的标准输出重定向到一个管道中。这样，你就可以通过代码来读取子进程的输出内容。
            # 例如，在后续代码中使用 process.communicate() 方法就可以获取这个管道中的内容。
        process = subprocess.Popen(prog_desc, stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
        # communicate()方法会等待子进程结束，并从子进程的标准输出和标准错误输出管道中读取数据。该方法返回一个元组，元组的第一个元素是子进程的标准输出内容，第二个元素是子进程的标准错误输出内容。
        # communicate() 方法会阻塞当前线程，直到子进程结束。
        ver, err = process.communicate()
        # wait() 方法等待子进程结束,返回状态码为0表示成功
        retcode = process.wait()
        if retcode == 0:
            # ver中存储着从管道中读取的字符串,以 bytes 类型存储,通过 strip() 方法先去掉首尾的空白字符串(空格,指标,换行,回车等),然后用decode()方法转换成字符串,str()方法是为了确保一定转换成了字符串加的一层保险
            # 注意 strip() 方法不会去掉字符串中间的空白字符,只处理首尾.
            git_info["version"] = str(ver.strip().decode())
            process = subprocess.Popen(prog_status, stdout=subprocess.PIPE,
                                       stderr=subprocess.PIPE)
            stat, err = process.communicate()
            # split() 按照指定的字符将字符串分割成多行
            # l.split(None, 1) 中 None 表示任意的空白字符都能作为换行的依据, 1表示只换行1次
            # 示例字符串
                # l = "hello   world  python"
                # result = l.split(None, 1)
                # print(result)
                # ['hello', 'world  python']

            status = [l.split(None, 1)
                      for l in str(stat.strip().decode()).split('\n')]
            retcode = process.wait()
            if retcode == 0:
                git_info["file_status"] = status
            else:
                logging.debug("Error getting git status: %s", err)
            git_info.update(_get_repo_info(gitdir))
            return git_info
        else:
            logging.debug("Error getting git version: %s", err)
    except:
        logging.debug("Exception on run: %s", traceback.format_exc())

    if from_file:
        git_info["version"] = get_version_from_file(klippy_src)
    return git_info
